(function () {
  class VNode {
    constructor(tag, text, attrs, children) {
      this.tag = tag;
      this.text = text;
      this.attrs = attrs;
      this.children = children;
    }
  }

  const build = function (vNode) {
    if (vNode.text) return vNode.$el = document.createTextNode(vNode.text);
    if (vNode.tag) {
      const $el = document.createElement(vNode.tag);
      handleAttrs(vNode, $el);
      vNode.children.forEach((child) => {
        $el.appendChild(build(child));
      });
      return vNode.$el = $el;
    }
  };
  const handleAttrs = function ({ attrs }, $el, preAttrs = {}) {
    if (preAttrs.class !== attrs.class || preAttrs['v-class'] !== attrs['v-class']) {
      let clsStr = '';
      if (attrs.class) clsStr += attrs.class;
      if (attrs['v-class']) clsStr += ' ' + attrs['v-class'];
      $el.className = clsStr;
    }
    if (attrs['v-on-click'] !== preAttrs['v-on-click']) { // 这里匿名函数总是会不等的
      if (attrs['v-on-click']) $el.onclick = attrs['v-on-click'];
    }
  };
  const patch = function (preVode, vNode) {
    if (preVode.tag === vNode.tag) {
      vNode.$el = preVode.$el;
      if (vNode.text) {
        if (vNode.text !== preVode.text) vNode.$el.textContent = vNode.text;
      } else {
        vNode.$el = preVode.$el;
        preVode.children.forEach((preChild, i) => { // TODO:
          patch(preChild, vNode.children[i]);
        });
        handleAttrs(vNode, vNode.$el, preVode.attrs);
      }
    } else {
      // TODO: 直接替换...
    }
  };

  class Vue {
    constructor({ el, data, tpl, methods }) {
      // set render
      if (el instanceof Element) {
        this.$el = el;
      } else {
        this.$el = document.querySelector(el);
      }
      const ast = parseHtml(tpl);
      const renderCode = generateRender(ast);
      this.render = new Function(renderCode);

      // set data
      this.data = proxy(data.call(this));

      // set proxy obj for data
      const proxyObj = new Proxy(this, {
        get(target, key) {
          if (key in target.data) return target.data[key];
          return target[key];
        },
        set(target, key, value) {
          if (!(key in target.data) && key in target) {
            target[key] = value;
          } else {
            target.data[key] = value;
          }
          return true;
        },
        has(target, key) {
          return (key in target) || (key in target.data);
        },
      });
      this._proxyObj = proxyObj;

      // bind methods
      Object.keys(methods).forEach((key) => {
        this[key] = methods[key].bind(proxyObj);
      });

      const updateComponent = () => {
        this._update(this._render());
      };

      watch(updateComponent, () => {/* noop */});
    }

    _update(vNode) {
      const preVode = this.preVode;
      if (preVode) {
        patch(preVode, vNode);
      } else {
        this.preVode = vNode;
        this.$el.appendChild(build(vNode));
      }
    }

    _render() {
      return this.render.call(this._proxyObj);
    }

    _c(tag, attrs, children) {
      return new VNode(tag, null, attrs, children);
    }

    _v(text) {
      return new VNode(null, text, null, null);
    }
  }

  Vue.new = function (opts) {
    return new Vue(opts)._proxyObj;
  };

  window.Vue2 = Vue;
})();
